/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.data.service.segments.relations.delete;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.unidata.mdm.core.type.calculables.CalculableHolder;
import org.unidata.mdm.core.type.calculables.ModificationBox;
import org.unidata.mdm.core.type.data.DataShift;
import org.unidata.mdm.core.type.data.OperationType;
import org.unidata.mdm.core.type.data.RecordStatus;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.core.util.SecurityUtils;
import org.unidata.mdm.data.context.DeleteRelationRequestContext;
import org.unidata.mdm.data.context.DeleteRequestContext;
import org.unidata.mdm.data.module.DataModule;
import org.unidata.mdm.data.service.impl.CommonRelationsComponent;
import org.unidata.mdm.data.service.impl.RelationComposerComponent;
import org.unidata.mdm.data.service.segments.ContainmentRelationSupport;
import org.unidata.mdm.data.type.apply.RelationDeleteChangeSet;
import org.unidata.mdm.data.type.apply.batch.impl.RelationDeleteBatchSet;
import org.unidata.mdm.data.type.calculables.impl.RelationRecordHolder;
import org.unidata.mdm.data.type.data.OriginRelation;
import org.unidata.mdm.data.type.data.OriginRelationInfoSection;
import org.unidata.mdm.data.type.data.RelationType;
import org.unidata.mdm.data.type.data.impl.OriginRelationImpl;
import org.unidata.mdm.data.type.keys.RelationEtalonKey;
import org.unidata.mdm.data.type.keys.RelationKeys;
import org.unidata.mdm.data.type.keys.RelationOriginKey;
import org.unidata.mdm.data.type.timeline.RelationTimeline;
import org.unidata.mdm.system.service.PlatformConfiguration;
import org.unidata.mdm.system.type.pipeline.Point;
import org.unidata.mdm.system.type.pipeline.Start;
import org.unidata.mdm.system.type.support.IdentityHashSet;

/**
 * @author Mikhail Mikhailov
 * Prepares delete context.
 */
@Component(RelationDeleteTimelineExecutor.SEGMENT_ID)
public class RelationDeleteTimelineExecutor extends Point<DeleteRelationRequestContext> implements ContainmentRelationSupport {
    /**
     * This segment ID.
     */
    public static final String SEGMENT_ID = DataModule.MODULE_ID + "[RELATION_DELETE_TIMELINE]";
    /**
     * Localized message code.
     */
    public static final String SEGMENT_DESCRIPTION = DataModule.MODULE_ID + ".relation.delete.timeline.description";
    /**
     * Composer component.
     */
    @Autowired
    private RelationComposerComponent relationComposerComponent;
    /**
     * The CC.
     */
    @Autowired
    private CommonRelationsComponent commonRelationsComponent;
    /**
     * PC.
     */
    @Autowired
    private PlatformConfiguration platformConfiguration;
    /**
     * Constructor.
     */
    public RelationDeleteTimelineExecutor() {
        super(SEGMENT_ID, SEGMENT_DESCRIPTION);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void point(DeleteRelationRequestContext ctx) {

        // Containments are processed by record services entirely
        // Updates timeline for periods
        if (ctx.relationType() == RelationType.CONTAINS) {
            processContainment(ctx);
        } else if (ctx.relationType() == RelationType.REFERENCES) {
            processReference(ctx);
        } else {
            processRelTo(ctx);
        }
    }

    private void processContainment(DeleteRelationRequestContext ctx) {

        Date ts = ctx.timestamp();
        RelationKeys relationKeys = ctx.relationKeys();
        String user = SecurityUtils.getCurrentUserName();

        DeleteRequestContext uCtx = ctx.containmentContext();
        RelationKeys keys = null;

        // Inactivate etalon
        if (ctx.isInactivateEtalon()) {

            // Timeline remains the same,
            // Just keys are changed.
            keys = RelationKeys.builder(relationKeys)
                .updateDate(ts)
                .updatedBy(user)
                .etalonKey(RelationEtalonKey.builder(relationKeys.getEtalonKey())
                        .status(RecordStatus.INACTIVE)
                        .build())
                .build();

        // Inactivate period
        } else {
            keys = relationKeys;
        }

        ctx.nextTimeline(mirror(keys, uCtx.nextTimeline()));
    }

    /**
     * Does real rel to etalon calculation.
     * @param ctx the context
     * @return etalon relation
     */
    private void processReference(DeleteRelationRequestContext ctx) {

        processRelTo(ctx);

        Timeline<OriginRelation> next = ctx.nextTimeline();
        RelationKeys keys = next.getKeys();

        // UN-10682 May be read from batch cache
        List<Timeline<OriginRelation>> oldVirtual = commonRelationsComponent.loadOrReuseCachedTimelines(ctx);
        List<Timeline<OriginRelation>> newVirtual = new ArrayList<>(oldVirtual);

        newVirtual.removeIf(timeline -> keys.getEtalonKey().getId().equals(timeline.getKeys().getEtalonKey().getId()));
        newVirtual.add(next);

        oldVirtual = commonRelationsComponent.buildVirtualTimelinesForReferences(oldVirtual);
        newVirtual = commonRelationsComponent.buildVirtualTimelinesForReferences(newVirtual);

        // Special case for references
        // Add this to 'current' and to 'next'
        // to enable post-processing by other segments
        ctx.previousReferences(oldVirtual);
        ctx.nextReferences(newVirtual);

        // Save for possible subsequent calls in case of batched execution
        RelationDeleteChangeSet set = ctx.changeSet();
        if (set instanceof RelationDeleteBatchSet) {
            ((RelationDeleteBatchSet) set)
                .addCachedReferenceTimelines(
                        keys.getEtalonKey().getFrom().getId(),
                        keys.getRelationName(), newVirtual);
        }
    }

    private void processRelTo(DeleteRelationRequestContext ctx) {

        Date ts = ctx.timestamp();
        RelationKeys relationKeys = ctx.relationKeys();
        String user = SecurityUtils.getCurrentUserName();
        OperationType operationType = ctx.operationType();

        Timeline<OriginRelation> current = ctx.currentTimeline();
        Timeline<OriginRelation> next = null;
        ModificationBox<OriginRelation> box = ctx.modificationBox();

        if (ctx.isWipe()) {
            next = new RelationTimeline(relationKeys);
        } else if (ctx.isInactivatePeriod()) {

            Set<CalculableHolder<OriginRelation>> revisions = new IdentityHashSet<>();
            List<CalculableHolder<OriginRelation>> result = new ArrayList<>();

            // 'current' is restricted to period bounds
            current
                .selectBy(ctx.getValidFrom(), ctx.getValidTo())
                .forEach(i -> {

                    List<CalculableHolder<OriginRelation>> existing = i.toList();
                    existing.stream()
                            .filter(v -> v.getStatus() == RecordStatus.ACTIVE)
                            .map(v -> {

                                // Sort out revisions, which were already added.
                                if (revisions.contains(v)) {
                                    return null;
                                }

                                revisions.add(v);

                                // Push upsert
                                RelationOriginKey rok = (RelationOriginKey) v.getOriginKey();
                                return new OriginRelationImpl()
                                        .withDataRecord(v.getValue())
                                        .withInfoSection(new OriginRelationInfoSection()
                                                .withRelationName(relationKeys.getRelationName())
                                                .withRelationType(relationKeys.getRelationType())
                                                .withRelationOriginKey(rok)
                                                .withValidFrom(ctx.getValidFrom())
                                                .withValidTo(ctx.getValidTo())
                                                .withFromEntityName(relationKeys.getFromEntityName())
                                                .withToEntityName(relationKeys.getToEntityName())
                                                .withStatus(RecordStatus.INACTIVE)
                                                .withShift(DataShift.PRISTINE)
                                                .withOperationType(operationType == null ? OperationType.DIRECT : operationType)
                                                .withMajor(platformConfiguration.getPlatformMajor())
                                                .withMinor(platformConfiguration.getPlatformMinor())
                                                .withCreateDate(rok.getCreateDate())
                                                .withUpdateDate(ts)
                                                .withCreatedBy(rok.getCreatedBy())
                                                .withUpdatedBy(user));
                            })
                            .filter(Objects::nonNull)
                            .map(RelationRecordHolder::new)
                            .collect(Collectors.toCollection(() -> result));
                });

            // Original keys traditionally remain unchanged until the end of the PL
            // Set the new keys state to the NEXT timeline
            RelationKeys nextKeys = RelationKeys.builder(relationKeys)
                .updateDate(ts)
                .updatedBy(user)
                .build();

            if (Objects.nonNull(box)) {
                box.toModifications().forEach((k, v) -> v.forEach(result::add));
            }

            // Not reducing the TL, because we have to know,
            // if there are still some active period or the rel may be deleted entirely
            next = current.merge(new RelationTimeline(nextKeys, result));

            // Refresh view
            next.forEach(i -> relationComposerComponent.toEtalon(nextKeys, i));
        } else if (ctx.isInactivateEtalon()) {

            // Timeline remains the same,
            // Just keys are changed.
            RelationKeys nextKeys
                = RelationKeys.builder(relationKeys)
                    .updateDate(ts)
                    .updatedBy(user)
                    .etalonKey(RelationEtalonKey.builder(relationKeys.getEtalonKey())
                            .status(RecordStatus.INACTIVE)
                            .build())
                    .build();

            List<CalculableHolder<OriginRelation>> target = new ArrayList<>(current.getCalculables());
            if (Objects.nonNull(box)) {
                box.toModifications().forEach((k, v) -> v.forEach(target::add));
            }

            // Refresh view
            next = new RelationTimeline(nextKeys, target);
            next.forEach(i -> relationComposerComponent.toEtalon(nextKeys, i));
        }

        ctx.nextTimeline(next);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean supports(Start<?, ?> start) {
        return DeleteRelationRequestContext.class.isAssignableFrom(start.getInputTypeClass());
    }
}
